package view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Polygon;

import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;

import model.ISPGraph;
import model.IVertex;
import model.SPGraph;

public class GraphicalView extends AbstractGraphView {
	
	// FIELDS
	private static final long serialVersionUID = -7402542819360834170L;
	
	private final int vertexRay = 15;
	
	private Map<Coord, IVertex> map1;
	private Map<IVertex, Coord> map2;
	
	private IVertex selectedVertex;
	
	// CONSTRUCTERS
	public GraphicalView(ISPGraph model) {
		super(model);
		((SPGraph) getModel()).addObserver(new Observer() {

			@Override
			public void update(Observable o, Object arg) {
				initCoords();
			}
			
		});
		initCoords();
		this.selectedVertex = null;
	}
	
	public GraphicalView() {
		this(new SPGraph());
	}
	
	// METHODS
	protected void paintComponent(Graphics g) {
		super.paintComponent(g); 
		drawVertexes(g);
		drawEdges(g);
	}
	
	protected void drawVertexes(Graphics g) {
		for (Coord c: this.map1.keySet()) {
			if (getModel().getCodeWords().contains(this.map1.get(c).getContent())) {
				g.setColor(Color.GRAY);
			} else {
				g.setColor(Color.LIGHT_GRAY);
			}
			g.fillOval(c.getX() - vertexRay, c.getY() - vertexRay, this.vertexRay*2, this.vertexRay*2);
	
			g.setColor(Color.BLACK);
			String text = this.map1.get(c).getContent();
			g.drawString(text, 
					c.getX()-(7*text.length()/2), c.getY());
		}
	}
	
	protected void drawEdges(Graphics g) {
		g.setColor(Color.BLACK);
		for (IVertex vDeparture: getModel().getDepartureVertexes()) {
			Coord cDeparture = getVertexCoord(vDeparture);
			for (IVertex vArrival: getModel().getVertexSuccessors(vDeparture)) {
				String label = getModel().getEdgeLabel(vDeparture, vArrival);
				if (vDeparture.equals(vArrival)) {
					int x = cDeparture.getX() - (this.vertexRay/2);
					int y = cDeparture.getY() - (this.vertexRay*3);
					
					g.drawString("epsylon", x, y);
					drawArrow(g, cDeparture.getX(), cDeparture.getY(), 
							cDeparture.getX(), cDeparture.getY(), 1);
				} else {
					Coord cArrival = getVertexCoord(vArrival);
					
					Coord tempCoord = new Coord(cDeparture.getX(),cArrival.getY());
					double co = tempCoord.getDistance(cDeparture);
					double h = cDeparture.getDistance(cArrival);
					double alpha = Math.asin(co/h);
					
					h = this.vertexRay;
					double ca = h * Math.cos(alpha);
					co = h * Math.sin(alpha);
					
					int xArrivalCoef = (cDeparture.getX() <= cArrival.getX()) ? -1 : 1;
					int yArrivalCoef = (cDeparture.getY() <= cArrival.getY()) ? -1 : 1;
					int xArrival = (int) (cArrival.getX() + (xArrivalCoef * ca));
					int yArrival = (int) (cArrival.getY() + (yArrivalCoef * co));
					
					int xDepartureCoef = (cDeparture.getX() <= cArrival.getX()) ? 1 : -1;
					int yDepartureCoef = (cDeparture.getY() <= cArrival.getY()) ? 1 : -1;
					int xDeparture = (int) (cDeparture.getX() + (xDepartureCoef * ca));
					int yDeparture = (int) (cDeparture.getY() + (yDepartureCoef * co));
					
					drawArrow(g, xDeparture, yDeparture, xArrival, yArrival, 1);
					
					h = cDeparture.getDistance(cArrival);
					ca = (h/2) * Math.cos(alpha);
					co = (h/2) * Math.sin(alpha);
					int xLabel = (int) (cArrival.getX() + (xArrivalCoef * ca));
					int yLabel = (int) (cArrival.getY() + (yArrivalCoef * co));
					if (label.equals("")) { 
						label = "epsylon";
					}
					g.drawString(label, xLabel, yLabel);
				}
			}
		}
	}
	
	protected void initCoords() {
		this.map1 = new HashMap<Coord, IVertex>();
		this.map2 = new HashMap<IVertex, Coord>();
		int vertexesNb = getModel().getVertexes().size();
		if (vertexesNb > 0) {
			Dimension componentSize = this.getPreferredSize();
			int margeX = 10 + (int) Math.floor(componentSize.width * 0.1);
			int margeY = 30 + (int) Math.floor(componentSize.height * 0.1);
			final int maxVertexesNbCol = 6;
			int vertexesNbCol = maxVertexesNbCol;
			if (vertexesNb < (2* maxVertexesNbCol)) {
				vertexesNbCol = (int) Math.ceil((((double) vertexesNb) / 2));
			}
			
			int vertexesNbLine = (int) Math.ceil(((double) vertexesNb) / ((double) vertexesNbCol));
			int vertexDeltaX = (componentSize.width - (2 * margeX)) 
								/ vertexesNbCol;
			int vertexDeltaY = (componentSize.height - (2 * margeY)) 
								/ vertexesNbLine;
			
			int x = margeX + vertexRay;
			int y = margeY + vertexRay;
			
			int xCpt = 0;
			for (IVertex v: getModel().getVertexes()) {
				Coord c = new Coord(x, y);
				this.map1.put(c, v);
				this.map2.put(v, c); 
				xCpt++;
				if (xCpt == vertexesNbCol) {
					xCpt = 0;
					x = margeX + vertexRay;
					y = y + vertexDeltaY;
				} else {
					x = x + vertexDeltaX;
				}
			}
		}
	}
	
	public Dimension getPreferredSize() {
		return new Dimension(450, 250);
	}
	
	public Set<Coord> getCoords() {
		return map1.keySet();
	}
	
	public IVertex getVertexAt(Coord c) {
		if (c == null) {
			throw new IllegalArgumentException("coord argument is null");
		}
		return map1.get(c);
	}
	
	public Coord getVertexCoord(IVertex v) {
		if (v == null) {
			throw new IllegalArgumentException("vertex argument is null");
		}
		if (!this.map2.containsKey(v)) {
			throw new IllegalArgumentException("vertex argument doesn't exist");
		}
		return map2.get(v);
	}
	
	public IVertex getCloserVertex(Coord c, double delta) {
		if (c == null) {
			throw new IllegalArgumentException("coord argument is null");
		}
		if (delta < 0) {
			throw new IllegalArgumentException("delta argument is < 0");
		}
		IVertex v = null;
		Coord tempCoord = null;
		double currentDist = Double.MAX_VALUE;
		double tempDist;
		
		for (Coord coord: this.map1.keySet()) {
			tempDist = c.getDistance(coord);
			if (tempDist <= delta) {
				if (tempDist < currentDist) {
					currentDist = tempDist;
					tempCoord = coord;
				}
			}
		}
		
		if (tempCoord != null) {
			v = this.map1.get(tempCoord);
		}
		
		return v;
	}
	
	public IVertex getSelectedVertex() {
		return this.selectedVertex;
	}
	
	/**
	 * Sets a coord to an existing vertex.
	 * 
	 * @param v the vertex to set it coord
	 * @param c the coord of the specified vertex
	 */
	public void setVertexCoord(IVertex v, Coord c) {
		if (v == null) {
			throw new IllegalArgumentException("vertex argument is null");
		}
		if (c == null) {
			throw new IllegalArgumentException("coord argument is null");
		}
		if (!this.map2.keySet().contains(v)) {
			throw new IllegalArgumentException("the specified vertex doesn't exist");
		}
		Coord tempCoord = getVertexCoord(v);
		this.map2.put(v, c);
		this.map1.remove(tempCoord);
		this.map1.put(c, v);
		repaint();
	}
	
	public void setSelectedVertex(IVertex v) {
		if (v == null) {
			throw new IllegalArgumentException("vertex argument is null");
		}
		if (!this.map2.containsKey(v)) {
			throw new IllegalArgumentException("the specified vertex doesn't exist");
		}
		this.selectedVertex = v;
	}
	
	public void unSelectVertex() {
		this.selectedVertex = null;
	}
	
	public void drawArrow(Graphics g2d, int xCenter, int yCenter, int x, int y, float stroke) {
		double aDir;
		if (xCenter == x && yCenter == y) {
			aDir = 2.3;
			y = y - this.vertexRay;
			int xOval = xCenter - (this.vertexRay/2);
			int yOval = yCenter - (this.vertexRay*3);
			g2d.drawOval(xOval, yOval, this.vertexRay, this.vertexRay*2);
		} else {
			aDir=Math.atan2(xCenter-x,yCenter-y);
			g2d.drawLine(x,y,xCenter,yCenter);
		}
		Polygon tmpPoly=new Polygon();
		int i1=12+(int)(stroke*2);
		int i2=6+(int)stroke;		
		tmpPoly.addPoint(x,y);		
		tmpPoly.addPoint(x+xCor(i1,aDir+.5),y+yCor(i1,aDir+.5));
		tmpPoly.addPoint(x+xCor(i2,aDir),y+yCor(i2,aDir));
		tmpPoly.addPoint(x+xCor(i1,aDir-.5),y+yCor(i1,aDir-.5));
		tmpPoly.addPoint(x,y);		
		g2d.drawPolygon(tmpPoly);
		g2d.fillPolygon(tmpPoly);	
	}
	
	public void updateView() {
		repaint();
	}
	
	public void setModel(ISPGraph model) {
		super.setModel(model);
		((SPGraph) getModel()).addObserver(new Observer() {

			@Override
			public void update(Observable o, Object arg) {
				initCoords();
			}
			
		});
		initCoords();
		updateView();
	}
	
	private int yCor(int len, double dir) {
		return (int)(len * Math.cos(dir));
	}
	
	private int xCor(int len, double dir) {
		return (int)(len * Math.sin(dir));
	}
	
}
